﻿namespace Ngs.Data
{
    #region Using Directives
    using System;
    using System.Collections.Generic;
    using System.Linq.Expressions;
    using System.Reflection;
    using Ngs.Reflection;
    #endregion

    public static class MapperExtensions
    {
        static MapperExtensions()
        {
        }

        /// <summary>
        /// Attempts to match properties based on names.
        /// </summary>
        /// <typeparam name="TSource"></typeparam>
        /// <typeparam name="TDestination"></typeparam>
        /// <param name="map"></param>
        /// <returns></returns>
        public static IMapper<TSource, TDestination> AutoMap<TSource, TDestination>(this IMapper<TSource, TDestination> map)
        {
            ExceptionFactory.ThrowIfArgumentNull(map);

            Type type1, type2;
            type1 = typeof(TSource);
            type2 = typeof(TDestination);

            PropertyInfo[] publicProperties = type2.GetProperties(BindingFlags.Public | BindingFlags.Instance);

            foreach (var prop in publicProperties)
            {
                PropertyInfo match = type1.GetProperty(prop.Name);

                if (match != null)
                {
                    if (!prop.PropertyType.IsClass || prop.PropertyType.Name == "String")
                    {
                        var getter = match.GetValue<TSource>();
                        var setter = prop.SetValue<TDestination>();
                        map.Field((TSource source, TDestination dest) => setter(dest, getter(source)));
                    }

                    //else
                    //{
                    //    // Todo: Implement Reference
                    //    // map = map.Reference(
                    //}

                }
            }
            return map;
        }

        #region Field Property Mapping Methods

        public static IMapper<TSource, TDestination> Field<TSource, TDestination, TSubSource, TSubDestination>(this IMapper<TSource, TDestination> mapper, Expression<Func<TDestination, TSubDestination>> accessor)
        {
            ExceptionFactory.ThrowIfArgumentNull(accessor);
            MemberExpression member = (MemberExpression)accessor.Body;

            // Create replacement parameter
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), accessor.Parameters[0].Name);
            MemberExpression body = Expression.PropertyOrField(parameter, member.Member.Name);
            var lambda = Expression.Lambda<Func<TSource, TSubSource>>(body, parameter);
            var getter = lambda.Compile();
            var prop = (PropertyInfo)body.Member;
            var setter = prop.SetValue<TDestination>();
            mapper.Field((TSource source, TDestination dest) => setter(dest, getter(source)));

            return mapper;
        }

        public static IMapper<TSource, TDestination> Field<TSource, TDestination>(this IMapper<TSource, TDestination> mapper, Action<TSource, TDestination> fieldMap)
        {
            ExceptionFactory.ThrowIfArgumentNull(fieldMap);
            mapper.FieldMap += fieldMap;
            return mapper;
        }
        #endregion

        #region ReferenceMapper Methods

        public static IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> MapAccessor<TSource, TDestination, TSubSource, TSubDestination>(this IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> mapper, Expression<Func<TDestination, TSubDestination>> accessor)
        {
            ExceptionFactory.ThrowIfArgumentNull(accessor);
            MemberExpression member = (MemberExpression)accessor.Body;
            mapper.Member = member.Member;

            // Create replacement parameter
            ParameterExpression parameter = Expression.Parameter(typeof(TSource), accessor.Parameters[0].Name);
            MemberExpression body = Expression.PropertyOrField(parameter, member.Member.Name);
            var lambda = Expression.Lambda<Func<TSource, TSubSource>>(body, parameter);
            mapper.ReferenceGetter = lambda.Compile();


            return mapper;
        }

        public static IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> Setter<TSource, TDestination, TSubSource, TSubDestination>(this IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> mapper, Action<TDestination, TSubDestination> subDest)
        {
            mapper.ReferenceSetter = subDest;
            return mapper;
        }

        public static IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> Reference<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
             Expression<Func<TDestination, TSubDestination>> accessor, IMapper<TSubSource, TSubDestination> subMapper)
            where TSubDestination : new()
        {
            return Reference<TSubSource, TSubDestination, TSource, TDestination>(mapper, accessor, subMapper.Execute);
        }

        public static IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> Reference<TSubSource, TSubDestination, TSource, TDestination>
            (this IMapper<TSource, TDestination> mapper, Expression<Func<TDestination, TSubDestination>> accessor, IMapper<TSubSource, TSubDestination> subMapper, Func<TSubDestination> factory)
        {
            return Reference<TSubSource, TSubDestination, TSource, TDestination>(mapper, accessor, subMapper.Execute, factory);
        }

        public static IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> Reference<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
            Expression<Func<TDestination, TSubDestination>> accessor, Action<TSubSource, TSubDestination> reference)
            where TSubDestination : new()
        {
            return Reference<TSubSource, TSubDestination, TSource, TDestination>(mapper, accessor, reference, () => new TSubDestination());
        }

        public static IReferenceMapper<TSource, TDestination, TSubSource, TSubDestination> Reference<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
            Expression<Func<TDestination, TSubDestination>> accessor, Action<TSubSource, TSubDestination> reference, Func<TSubDestination> factory)
        {
            var map = new ReferenceMapper<TSource, TDestination, TSubSource, TSubDestination>();
            map.MapperSetup(mapper, reference, factory);
            map.MapAccessor(accessor);

            return map;
        }
        #endregion

        #region ReferenceListMapper Methods

        public static IReferenceListMapper<TSource, TDestination, TSubSource, TSubDestination> ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
            Expression<Func<TDestination, ICollection<TSubDestination>>> accessor, IMapper<TSubSource, TSubDestination> subMapper)
            where TSubDestination : new()
        {
            return ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(mapper, accessor, subMapper.Execute);
        }

        public static IReferenceListMapper<TSource, TDestination, TSubSource, TSubDestination> ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
            Expression<Func<TDestination, ICollection<TSubDestination>>> accessor, IMapper<TSubSource, TSubDestination> subMapper, Func<TSubDestination> factory)
        {
            return ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(mapper, accessor, subMapper.Execute, factory);
        }

        public static IReferenceListMapper<TSource, TDestination, TSubSource, TSubDestination> ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
            Expression<Func<TDestination, ICollection<TSubDestination>>> accessor, Action<TSubSource, TSubDestination> reference)
            where TSubDestination : new()
        {
            return ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(mapper, accessor, reference, () => new TSubDestination());
        }

        public static IReferenceListMapper<TSource, TDestination, TSubSource, TSubDestination> ReferenceList<TSubSource, TSubDestination, TSource, TDestination>(this IMapper<TSource, TDestination> mapper,
            Expression<Func<TDestination, ICollection<TSubDestination>>> accessor, Action<TSubSource, TSubDestination> reference, Func<TSubDestination> factory)
        {
            var map = new ReferenceListMapper<TSource, TDestination, TSubSource, TSubDestination>();
            map.MapperSetup(mapper, reference, factory);
            map.MapAccessor(accessor);

            return map;
        }
        #endregion

        private static void MapperSetup<TSubSource, TSubDestination, TSource, TDestination>(this AssociationMapper<TSource, TDestination, TSubSource, TSubDestination> map, IMapper<TSource, TDestination> mapper, Action<TSubSource, TSubDestination> reference, Func<TSubDestination> factory)
        {
            // First Clone all maps
            map.FieldMap = mapper.FieldMap;
            // Treat old mapper as field map and add to list.
            map.FieldMap += mapper.Execute;
            map.ReferenceMap += reference;
            map.SubDestinationFactory(factory);
        }
    }
}
